﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using Pfz.Caching;
using Pfz.Extensions;
using Pfz.Threading;

namespace Pfz.Remoting.Udp
{
	/// <summary>
	/// This is a connection listener that accepts both a tcp/ip connection and a bound udp connection with it.
	/// </summary>
	public sealed class TcpWithUdpListener:
		ThreadSafeDisposable,
		IConnectionListener<TcpWithUdpConnection>,
		IUdpConnectionListener<TcpWithUdpConnection>
	{
		private DisposableTcpListener _tcpListener;
		private GuaranteedUdpListener _udpListener;
		private ManagedAutoResetEvent _acceptedConnectionsEvent = new ManagedAutoResetEvent();
		private Queue<TcpWithUdpConnection> _acceptedNotReturnedConnections = new Queue<TcpWithUdpConnection>();
		internal HashSet<TcpWithUdpConnection> _activeConnections = new HashSet<TcpWithUdpConnection>();
		private Dictionary<Guid, _PendingConnection> _pendingConnections = new Dictionary<Guid, _PendingConnection>();

		/// <summary>
		/// Creates a new instance of this listener and configures how connections will work.
		/// </summary>
		public TcpWithUdpListener(IPAddress address, int port, bool hasTcpDelay=true, int bufferedWriteStreamSize=-1, int receiveBufferSize=-1, int sendBufferSize=-1, int pendingGuaranteedDeliveryCount=57)
		{
			try
			{
				_tcpListener = new DisposableTcpListener(address, port, hasTcpDelay, bufferedWriteStreamSize, receiveBufferSize, sendBufferSize);
				_udpListener = new GuaranteedUdpListener(port+1, pendingGuaranteedDeliveryCount);
				UnlimitedThreadPool.Run(_TcpAccepter, _tcpListener);
				UnlimitedThreadPool.Run(_UdpAccepter, _udpListener);
				GCUtils.Collected += _RemoveOldPendings;
			}
			catch
			{
				Dispose();
				throw;
			}
		}

		/// <summary>
		/// Releases this listener and all active connections.
		/// </summary>
		protected override void  Dispose(bool disposing)
		{
			if (disposing)
			{
				GCUtils.Collected -= _RemoveOldPendings;

				var pendingConnections = _pendingConnections;
				if (pendingConnections != null)
				{
					_pendingConnections = null;
					foreach(var pendingConnection in pendingConnections.Values)
						pendingConnection.Dispose();
				}

				Disposer.DisposeCollection(ref _acceptedNotReturnedConnections);
				Disposer.DisposeCollection(ref _activeConnections);
				Disposer.Dispose(ref _tcpListener);
				Disposer.Dispose(ref _udpListener);
				Disposer.Dispose(ref _acceptedConnectionsEvent);
			}

			base.Dispose(disposing);
		}

		private void _RemoveOldPendings()
		{
			try
			{
				lock(DisposeLock)
				{
					if (WasDisposed)
						return;

					List<Guid> toRemove = null;
					DateTime toRemoveTime = DateTime.Now - TimeSpan.FromMinutes(1);
					foreach(var pendingConnection in _pendingConnections)
					{
						if (pendingConnection.Value._dateTime < toRemoveTime)
						{
							if (toRemove == null)
								toRemove = new List<Guid>();

							toRemove.Add(pendingConnection.Key);
						}
					}

					if (toRemove != null)
						foreach(var key in toRemove)
							_pendingConnections.Remove(key);
				}
			}
			catch(OutOfMemoryException)
			{
			}
		}

		/// <summary>
		/// Tries to accept a connection.
		/// </summary>
		public TcpWithUdpConnection TryAccept()
		{
			var acceptedConnectionsEvent = _acceptedConnectionsEvent;
			if (acceptedConnectionsEvent == null)
				return null;

			while(true)
			{
				lock(DisposeLock)
				{
					if (WasDisposed)
						return null;

					if (_acceptedNotReturnedConnections.Count > 0)
						return _acceptedNotReturnedConnections.Dequeue();
				}

				acceptedConnectionsEvent.WaitOne();
			}
		}

		/// <summary>
		/// Gets an array with all active connections.
		/// </summary>
		public TcpWithUdpConnection[] GetConnections()
		{
			lock(DisposeLock)
			{
				if (WasDisposed)
					return null;

				return _activeConnections.ToArray();
			}
		}

		private void _TcpAccepter(DisposableTcpListener listener)
		{
			try
			{
				while(!WasDisposed)
				{
					var connection = listener.TryAccept();
					if (connection == null)
						return;

					try
					{
						UnlimitedThreadPool.Run(_TcpAccepted, connection);
					}
					catch
					{
						connection.Dispose();
						return;
					}
				}
			}
			catch
			{
			}
			finally
			{
				Dispose();
			}
		}
		private void _TcpAccepted(DisposableTcpConnection connection)
		{
			try
			{
				byte[] guidBytes = new byte[16];
				var stream = connection.Stream;

				bool canTimeout = stream.CanTimeout;
				if (canTimeout)
					stream.ReadTimeout = 60000;

				stream.FullRead(guidBytes);

				if (canTimeout)
					stream.ReadTimeout = Timeout.Infinite;

				Guid guid = new Guid(guidBytes);
				lock(DisposeLock)
				{
					if (WasDisposed)
					{
						connection.Dispose();
						return;
					}

					var pendingConnection = _pendingConnections.GetValueOrDefault(guid);
					if (pendingConnection == null)
					{
						connection.Dispose();
						return;
					}

					_pendingConnections.Remove(guid);
					var acceptedConnection = new TcpWithUdpConnection(this, connection, pendingConnection._udp);
					try
					{
						_acceptedNotReturnedConnections.Enqueue(acceptedConnection);
						_acceptedConnectionsEvent.Set();
					}
					catch
					{
						acceptedConnection.Dispose();
						connection.Dispose();
						return;
					}
				}
			}
			catch
			{
				connection.Dispose();
			}
		}

		private void _UdpAccepter(GuaranteedUdpListener listener)
		{
			try
			{
				while(!WasDisposed)
				{
					var connection = listener.TryAccept();
					if (connection == null)
						return;

					try
					{
						var guid = Guid.NewGuid();
						var bytes = guid.ToByteArray();

						lock(DisposeLock)
						{
							if (WasDisposed)
							{
								connection.Dispose();
								return;
							}

							var pendingConnection = new _PendingConnection(connection);
							_pendingConnections[guid] = pendingConnection;
						}

						connection.GuaranteedSend(bytes, 0, bytes.Length);
					}
					catch
					{
						connection.Dispose();
						return;
					}
				}
			}
			catch
			{
			}
			finally
			{
				Dispose();
			}
		}
	}
}
